package seismosurfer.graphics;

import java.awt.BasicStroke;
import java.awt.Rectangle;
import java.awt.TexturePaint;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeSupport;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.StringTokenizer;

import seismosurfer.data.MacroData;
import seismosurfer.data.QuakeData;
import seismosurfer.data.SiteData;

import com.bbn.openmap.image.BufferedImageHelper;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PropUtils;

/**
 * An extension to DrawingAttributes that handles attributes
 * used by the Seismo-Surfer layers.
 *
 */
public class SeismoDrawingAttributes extends DrawingAttributes {
    
    private static final long serialVersionUID = -2623651170576125841L;

    public static final float defaultMinMag = -10f;

    public static final float defaultMaxMag = 10f;

    private float minMag = defaultMinMag;

    private float maxMag = defaultMaxMag;

    public static final float defaultMinDepth = 0f;

    public static final float defaultMaxDepth = 1000f;

    private float minDepth = defaultMinDepth;

    private float maxDepth = defaultMaxDepth;

    public static final float defaultMinIntensity = 0f;

    public static final float defaultMaxIntensity = 12f;

    private float minIntensity = defaultMinIntensity;

    private float maxIntensity = defaultMaxIntensity;

    public static final int defaultMinPopulation = 0;

    public static final int defaultMaxPopulation = 100000000;

    private int minPopulation = defaultMinPopulation;

    private int maxPopulation = defaultMaxPopulation;

    public static final String changeFillPaintProperty = "changeFillPaint";

    public static final String changePointTypeProperty = "changePointType";

    public static final String changePointRadiusProperty = "changePointRadius";

    protected boolean changeFillPaint = true;

    protected boolean changePointType = true;

    protected boolean changePointRadius = true;

    public static final String PointTypeProperty = "pointType";

    public static final String PointRadiusProperty = "pointRadius";

    public static final String MagnitudeProperty = "magnitude";

    public static final String DepthProperty = "depth";

    public static final String IntensityProperty = "intensity";

    public static final String PopulationProperty = "population";

    public static final String MacroProperty = "macroSeismic";

    public static final String InfoProperty = "info";

    protected boolean forMacro = false;

    protected boolean forInfo = false;

    protected String pointType = SeismoPoint.CIRCLE;

    protected int pointRadius = SeismoPoint.DEFAULT_RADIUS;

    public SeismoDrawingAttributes() {
        super();
        setProperties(null, null);
    }

    public SeismoDrawingAttributes(Properties props) {
        super(props);
        setProperties(null, props);
    }

    public SeismoDrawingAttributes(String prefix, Properties props) {
        super(prefix, props);
        setProperties(prefix, props);
    }

    public boolean isForMacro() {
        return forMacro;
    }

    public boolean isForInfo() {
        return forInfo;
    }

    public void setChangeFillPaint(boolean change) {
        changeFillPaint = change;
    }

    public boolean canChangeFillPaint() {
        return changeFillPaint;
    }

    public void setChangePointType(boolean change) {
        changePointType = change;
    }

    public boolean canChangePointType() {
        return changePointType;
    }

    public void setChangePointRadius(boolean change) {
        changePointRadius = change;
    }

    public boolean canChangePointRadius() {
        return changePointRadius;
    }

    public void setIntensityBounds(float min, float max) {

        if (min >= max) {
            return;
        }

        if (!(min < defaultMinIntensity)) {
            minIntensity = min;

        }

        if (!(max > defaultMaxIntensity)) {
            maxIntensity = max;            
        }
    }

    public void setPopulationBounds(int min, int max) {

        if (min >= max) {
            return;
        }

        if (!(min < defaultMinPopulation)) {
            minPopulation = min;

        }

        if (!(max > defaultMaxPopulation)) {
            maxPopulation = max;            
        }
    }

    public void setMagnitudeBounds(float min, float max) {

        if (min >= max) {
            return;
        }

        if (!(min < defaultMinMag)) {
            minMag = min;

        }

        if (!(max > defaultMaxMag)) {
            maxMag = max;            
        }
    }

    public void setDepthBounds(float min, float max) {

        if (min >= max) {
            return;
        }

        if (!(min < defaultMinDepth)) {
            minDepth = min;

        }

        if (!(max > defaultMaxDepth)) {
            maxDepth = max;            
        }
    }

    /**
     * Checks if intensity is within [min, max) bounds.
     * 
     */ 
    public boolean isIntensityWithinBounds(float intensity) {        

        if ((intensity >= minIntensity) && (intensity < maxIntensity)) {
            return true;
        } else {
            return false;
        }

    }

    /**
     * Checks if population is within [min, max) bounds.
     * 
     */
    public boolean isPopulationWithinBounds(int population) {

        if ((population >= minPopulation) && (population < maxPopulation)) {
            return true;
        } else {
            return false;
        }

    }

    /**
     * Checks if magnitude is within [min, max) bounds.
     * 
     */
    public boolean isMagWithinBounds(float mag) {        

        if ((mag >= minMag) && (mag < maxMag)) {
            return true;
        } else {
            return false;
        }

    }

    /**
     * Checks if depth is within [min, max) bounds.
     * 
     */
    public boolean isDepthWithinBounds(float depth) {        

        if ((depth >= minDepth) && (depth < maxDepth)) {
            return true;
        } else {
            return false;
        }

    }

    public void setPointRadius(int radius) {
        pointRadius = radius;
    }

    public int getPointRadius() {
        return pointRadius;
    }

    public void setPointType(String type) {

        if (type.equals(SeismoPoint.CIRCLE)) {
            pointType = type;
        } else if (type.equals(SeismoPoint.RECTANGLE)) {
            pointType = type;
        } else if (type.equals(SeismoPoint.STAR)) {
            pointType = type;
        } else if (type.equals(SeismoPoint.CROSS)) {
            pointType = type;
        } else if (type.equals(SeismoPoint.TRIANGLE)) {
            pointType = type;
        } else {
            pointType = SeismoPoint.CIRCLE;

        }
    }

    public String getPointType() {
        return pointType;
    }
    
    /**
     * Set all the attributes for the graphic that are contained
     * within this SeismoDrawingAttributes class.
     * 
     */
    public void setTo(OMGraphic graphic) {

        if (graphic.getAppObject() instanceof QuakeData) {

            QuakeData qd = (QuakeData) graphic.getAppObject();

            if (isForMacro()) {
                if (!qd.getHasMacroseismic()) {
                    return;
                }
            }

            if (isForInfo()) {
                if (!qd.getHasInfo()) {
                    return;
                }
            }            

            if (!isMagWithinBounds((float) qd.getMagnitude())) {                
                return;
            }
            
            if (!isDepthWithinBounds((float) qd.getDepth())) {                
                return;
            }
        }

        else if (graphic.getAppObject() instanceof MacroData) {

            MacroData md = (MacroData) graphic.getAppObject();

            if (!isIntensityWithinBounds(md.getIntensity())) {
                return;
            }

        }

        else if (graphic.getAppObject() instanceof SiteData) {

            SiteData sd = (SiteData) graphic.getAppObject();

            if (!isPopulationWithinBounds(sd.getPopulation())) {
                return;
            }

        }

        setOMGraphicEdgeAttributes(graphic);
        
        if (fillPattern != null
                && (fillPaint == null || OMGraphic.isClear(fillPaint))) {
            graphic.setFillPaint(fillPattern);
        } else {
            if (graphic instanceof SeismoPoint) {
                if (canChangeFillPaint()) {
                    ((SeismoPoint) graphic).setFillPaint(fillPaint);
                }
            } else {
                graphic.setFillPaint(fillPaint);
                graphic.setTextureMask(fillPattern);
            }
        }

        graphic.setMatted(matted);
        graphic.setMattingPaint(mattingPaint);

        if (graphic instanceof SeismoPoint) {
            
            if (canChangePointRadius()) {
                ((SeismoPoint) graphic).setRadius(pointRadius);
            }
            if (canChangePointType()) {
                ((SeismoPoint) graphic).setPointType(pointType);
            }            
        }

    }
    
    /**
     * Sets the properties for the SeismoDrawingAttributes.
     */
    public void setProperties(String prefix, Properties props) {        

        propertyChangeSupport = new PropertyChangeSupport(this);
        setPropertyPrefix(prefix);

        if (props == null) {
            return;
        }

        String realPrefix = PropUtils.getScopedPropertyPrefix(prefix);

        // Set up the drawing attributes.

        forMacro = PropUtils.booleanFromProperties(props, realPrefix
                + MacroProperty, forMacro);

        forInfo = PropUtils.booleanFromProperties(props, realPrefix
                + InfoProperty, forInfo);

        changeFillPaint = PropUtils.booleanFromProperties(props, realPrefix
                + changeFillPaintProperty, changeFillPaint);

        changePointRadius = PropUtils.booleanFromProperties(props, realPrefix
                + changePointRadiusProperty, changePointRadius);

        changePointType = PropUtils.booleanFromProperties(props, realPrefix
                + changePointTypeProperty, changePointType);        

        // Magnitude Property

        java.util.List mags = PropUtils.parseSpacedMarkers(props
                .getProperty(prefix + MagnitudeProperty));

        float min = minMag;
        float max = maxMag;
        String value;

        if (!mags.isEmpty()) {

            if (mags.size() != 2) {
                System.out.println("Give the magnitude bounds [min, max).");
            } else {

                try {
                    value = (String) mags.get(0);
                    min = Float.valueOf(value).floatValue();

                    value = (String) mags.get(1);
                    max = Float.valueOf(value).floatValue();
                    
                } catch (NumberFormatException nfe) {
                    Debug
                            .error("QuakeAttributes.init: Number format exception for min or max");
                    min = minMag;
                    max = maxMag;
                }

                setMagnitudeBounds(min, max);

            }
        }

        // Depth property

        java.util.List depths = PropUtils.parseSpacedMarkers(props
                .getProperty(prefix + DepthProperty));

        min = minDepth;
        max = maxDepth;

        if (!depths.isEmpty()) {

            if (depths.size() != 2) {
                System.out.println("Give the depth bounds [min, max).");
            } else {

                try {
                    value = (String) depths.get(0);
                    min = Float.valueOf(value).floatValue();

                    value = (String) depths.get(1);
                    max = Float.valueOf(value).floatValue();
                    
                } catch (NumberFormatException nfe) {
                    Debug
                            .error("QuakeAttributes.init: Number format exception for min or max");
                    min = minDepth;
                    max = maxDepth;
                }

                setDepthBounds(min, max);

            }
        }

        // Intensity property

        java.util.List intensities = PropUtils.parseSpacedMarkers(props
                .getProperty(prefix + IntensityProperty));

        min = minIntensity;
        max = maxIntensity;

        if (!intensities.isEmpty()) {

            if (intensities.size() != 2) {
                System.out.println("Give the intensities bounds [min, max).");
            } else {

                try {
                    value = (String) intensities.get(0);
                    min = Float.valueOf(value).floatValue();

                    value = (String) intensities.get(1);
                    max = Float.valueOf(value).floatValue();
                    
                } catch (NumberFormatException nfe) {
                    Debug
                            .error("QuakeAttributes.init: Number format exception for min or max");
                    min = minIntensity;
                    max = maxIntensity;
                }

                setIntensityBounds(min, max);

            }
        }

        // Population Property

        java.util.List pops = PropUtils.parseSpacedMarkers(props
                .getProperty(prefix + PopulationProperty));

        int minPop = minPopulation;
        int maxPop = maxPopulation;

        if (!pops.isEmpty()) {

            if (pops.size() != 2) {
                System.out.println("Give the population bounds [min, max).");
            } else {

                try {
                    value = (String) pops.get(0);
                    minPop = Integer.valueOf(value).intValue();

                    value = (String) pops.get(1);
                    maxPop = Integer.valueOf(value).intValue();

                } catch (NumberFormatException nfe) {
                    Debug
                            .error("QuakeAttributes.init: Number format exception for min or max");
                    minPop = minPopulation;
                    maxPop = maxPopulation;
                }

                setPopulationBounds(minPop, maxPop);

            }
        }

        linePaint = PropUtils.parseColorFromProperties(props, realPrefix
                + linePaintProperty, linePaint);

        selectPaint = PropUtils.parseColorFromProperties(props, realPrefix
                + selectPaintProperty, selectPaint);

        mattingPaint = PropUtils.parseColorFromProperties(props, realPrefix
                + mattingPaintProperty, mattingPaint);

        fillPaint = PropUtils.parseColorFromProperties(props, realPrefix
                + fillPaintProperty, fillPaint);

        matted = PropUtils.booleanFromProperties(props, realPrefix
                + mattedProperty, matted);

        pointRadius = PropUtils.intFromProperties(props, realPrefix
                + PointRadiusProperty, pointRadius);        

        pointType = props
                .getProperty(realPrefix + PointTypeProperty, pointType);        

        float lineWidth;
        boolean basicStrokeDefined = false;

        if (stroke != null && stroke instanceof BasicStroke) {
            basicStrokeDefined = true;
        }

        lineWidth = PropUtils.floatFromProperties(props, realPrefix
                + lineWidthProperty,
                (basicStrokeDefined ? ((BasicStroke) stroke).getLineWidth()
                        : defaultLineWidth));

        baseScale = PropUtils.floatFromProperties(props, realPrefix
                + baseScaleProperty, baseScale);

        // Look for a dash pattern properties to come up with a stroke
        String dPattern = props.getProperty(realPrefix + dashPatternProperty);
        if (basicStrokeDefined && dPattern != null && !dPattern.equals("")) {
            float dashPhase;
            float[] lineDash;
            // OK, it exists, come up with a stroke.
            try {
                StringTokenizer t = new StringTokenizer(dPattern);
                int arraySize = t.countTokens();
                lineDash = new float[arraySize];

                int dashCount = 0;
                while (t.hasMoreTokens()) {
                    String segment = t.nextToken();
                    lineDash[dashCount++] = Float.parseFloat(segment);
                    if (Debug.debugging("drawingattributes")) {
                        Debug.output("read " + segment);
                    }
                }

            } catch (NoSuchElementException nsee) {
                Debug
                        .error("DrawingAttributes.init: dash pattern attributes wrong - should be dashPattern=(number pixels on) (number pixels off)");
                lineDash = null;
            } catch (NumberFormatException nfe) {
                Debug
                        .error("DrawingAttributes.init: Number format exception for dashPattern");
                lineDash = null;
            } catch (NullPointerException npe) {
                Debug
                        .error("DrawingAttributes.init: Caught null pointer exception - probably resulting from non-float number format exception for dashPattern");
                lineDash = null;
            }

            if (lineDash == null) {
                if (basicStrokeDefined) {
                    lineDash = ((BasicStroke) stroke).getDashArray();
                } else {
                    lineDash = new float[2];
                    lineDash[0] = defaultDashLength;
                    lineDash[1] = defaultDashLength;
                }
            }

            int dashCount = 0;
            for (int x = 0; x < lineDash.length; x++) {
                dashCount += lineDash[x];
            }
            if (dashCount == 0) {
                lineDash = null;
            }

            String dPhase = props.getProperty(realPrefix + dashPhaseProperty);
            if (dPhase != null && !dPhase.equals("")) {
                try {
                    dashPhase = Float.valueOf(dPhase).floatValue();
                } catch (NumberFormatException nfe) {
                    Debug
                            .error("DrawingAttributes.init: Number format exception for dashPhase");
                    dashPhase = defaultDashPhase;
                }
            } else {
                if (basicStrokeDefined) {
                    dashPhase = ((BasicStroke) stroke).getDashPhase();
                } else {
                    dashPhase = defaultDashPhase;
                }
            }

            setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_BUTT,
                    BasicStroke.JOIN_MITER, 10.0f, lineDash, dashPhase));

        } else if (basicStrokeDefined) {

            BasicStroke bs = (BasicStroke) getStroke();
            int cap = bs.getEndCap();
            int join = bs.getLineJoin();
            float miterLimit = bs.getMiterLimit();
            float[] dash = bs.getDashArray();
            float dashPhase = bs.getDashPhase();

            setStroke(new BasicStroke(lineWidth, cap, join, miterLimit, dash,
                    dashPhase));

        }

        // OK, Fill pattern next...
        fPattern = props.getProperty(realPrefix + fillPatternProperty);
        if (fPattern != null && !fPattern.equals("")) {

            try {

                URL textureImageURL = PropUtils.getResourceOrFileOrURL(this,
                        fPattern);

                if (textureImageURL != null) {

                    BufferedImage bi = BufferedImageHelper.getBufferedImage(
                            textureImageURL, 0, 0, -1, -1);

                    fillPattern = new TexturePaint(bi, new Rectangle(0, 0, bi
                            .getWidth(), bi.getHeight()));
                }
            } catch (MalformedURLException murle) {
                Debug.error("DrawingAttributes.init: bad texture URL - \n     "
                        + realPrefix + fillPatternProperty);
                fillPattern = null;
            } catch (InterruptedException ie) {
                Debug
                        .error("DrawingAttributes.init: bad problems getting texture URL - \n"
                                + ie);
                fillPattern = null;
            }
        }
    }

    public void setFrom(SeismoDrawingAttributes qa) {

        matted = qa.isMatted();
        mattingPaint = qa.getMattingPaint();

        linePaint = qa.getLinePaint();
        baseScale = qa.getBaseScale();

        selectPaint = qa.getSelectPaint();
        fillPaint = qa.getFillPaint();
        fillPattern = qa.getFillPattern();

        // Need to put this in to keep the gui up to date. Calling
        // setStroke fires off a propertyChange reaction that
        // potentially harms other parameters, like renderType.
        stroke = qa.getStroke();

        pointRadius = qa.getPointRadius();
        pointType = qa.getPointType();

        // Don't want to call this here, it is CPU intensive.
        // resetGUI should be called only when the GUI needs to be
        // updated.
        // resetGUI();

        if (propertyChangeSupport != null) {
            propertyChangeSupport.firePropertyChange("all", true, true);
        }
    }

}
